package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private CacheClient cacheClient;
    //    创建一个有十个线程的线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    @Override
    @Transactional
    public Result updateShop(Shop shop) {
        /*	在修改值时，先把缓存数据删除了，那么下次查询进来时，缓存中就查不到了，
        就会在数据库中查询，然后 重新缓存到缓存中，实现了缓存数据主动更新*/
        final Long id = shop.getId();
        if (null == id) {
            return Result.fail("缺少必要参数id");
        }
//        1  第一步先更新数据库
        updateById(shop);
//        2 第二步删除缓存数据
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
        return Result.ok();
    }

    /*
     *
     * @Title: 缓存空值 解决缓存穿透问题
     * @Description
     * @Author chs
     * @Date  2024/3/6 11:12
     * @param null
     * @Return
     * @Throws
     **/
    public Shop queryWithPassThrough(Integer id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
//        1查询redis中是否有数据
        String s = stringRedisTemplate.opsForValue().get(key);
//        2有数据直接返回
        if (StrUtil.isNotBlank(s)) {
//            返序列化成对象好久返回到前台
            return JSONUtil.toBean(s, Shop.class);
        }
        if (null == s) {
            return null;
        }
        //        3没数据去数据库查询  如果数据库没有就直接返回报错
        Shop shop = getById(id);
        if (null == shop) {
//            防止缓存击穿，当数据库中数据不存在时，将空值存在缓冲中
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
//        4将数据库查询到的数据直接返存入缓存
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//        5返回查询结果
        return shop;
    }

    @Override
    public Result queryShopById(Long id) {
//         设置空值 处理缓存穿透
//        Shop shop = queryWithPassThrough(Math.toIntExact(id));
//         Shop shop = cacheClient.queryWithPassThrough(Shop.class, RedisConstants.CACHE_SHOP_KEY, id, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);


        //        互斥锁解决   缓存击穿
        Shop shop = queryWithMutex(id);
//       逻辑过期解决 缓存击穿问题
//        Shop shop = queryWithLogicalExpire(id);
//        Shop shop = cacheClient.queryWithLogicalExpire(Shop.class, id, RedisConstants.CACHE_SHOP_KEY, this::getById, 30L, TimeUnit.MINUTES);
        if (null == shop) {
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);

    }

    /**
     * 逻辑过期解决缓存击穿问题
     *
     * @param id
     * @Title: queryWithLogicalExpire
     * @Description
     * @Author chs
     * @Date 2024/3/6 15:16
     * @Return com.hmdp.entity.Shop
     * @Throws
     **/

    private Shop queryWithLogicalExpire(Long id) {
        String key = RedisConstants.LOGIN_USER_KEY + id;
//        1查询缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
//        2判断缓存是否存在
        if (StrUtil.isBlank(shopJson)) {
            return null;
        }
//        返序列化
        RedisData redisData = BeanUtil.toBean(shopJson, RedisData.class);
//        获取店铺信息
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
//        获取过期时间
        LocalDateTime expireTime = redisData.getExpireTime();
//        3根据过期时间判断缓存是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
//        3.1未过期返回数据
            return shop;
        }
//        3.2已过期 获取锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        Boolean isLock = this.tryLock(lockKey);
//        获取锁成功
        if (isLock) {
            CACHE_REBUILD_EXECUTOR.submit(() -> {
//                刷新缓存数据
                try {
//                   4获取锁成功开启新线程跟新线程数据
                    this.saveSHop2Redis(id, 10L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
//                  5释放锁
                    unLock(lockKey);
                }
            });
        }
//        6返回缓存数据
        return shop;

    }

    /**
     * 逻辑过期-从数据库中获取最新的商户细腻
     *
     * @param id            商户id
     * @param expireSeconds 逻辑过期时间单位秒，从当前时间开始算多少秒后过期
     * @Title: saveSHop2Redis
     * @Description
     * @Author chs
     * @Date 2024/3/6 16:00
     * @Return void
     * @Throws
     **/
    public void saveSHop2Redis(Long id, Long expireSeconds) {
//      查店铺信息
        Shop shop = getById(id);
//     封装过期时间
        RedisData data = new RedisData();
        data.setData(LocalDateTime.now().plusSeconds(expireSeconds));
        data.setData(shop);
//     写入redis
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(data));
    }

    /**
     * 互斥锁解决缓存击穿问题
     *
     * @param id
     * @Title: queryWithMutex
     * @Description
     * @Author chs
     * @Date 2024/3/6 11:11
     * @Return com.hmdp.entity.Shop
     * @Throws
     **/
    private Shop queryWithMutex(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
//        1查询redis中是否有数据
        String s = stringRedisTemplate.opsForValue().get(key);
//        2缓存有数据直接返回
        if (StrUtil.isNotBlank(s)) {
//            返序列化成对象好久返回到前台
            return JSONUtil.toBean(s, Shop.class);
        }
        Shop shop = null;
//        3.1获取锁
        String lock = RedisConstants.LOCK_SHOP_KEY + id;
        try {
            Boolean tryLock = this.tryLock(lock);
            if (!tryLock) {
//            3.2获取说锁失败休眠50毫秒
                Thread.sleep(50);
//        3.3休眠后递归调佣直到获取数据成功
                queryWithMutex(id);
            }
//        4 获取成功查询数据库
            shop = getById(id);
            if (null == shop) {
//           防止缓存击穿，当数据库中数据不存在时，将空值存在缓冲中
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
//         5将数据库查询到的数据直接返存入缓存
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
//        6释放锁
            this.unLock(lock);
        }
//        7 返回查询结果
        return shop;
    }

    /**
     * 尝试获取锁
     * 过期时间2秒钟
     *
     * @param key 锁的key
     * @Title: tryLock
     * @Description
     * @Author chs
     * @Return Boolean 获取成功返回 true
     * @Throws
     **/
    public Boolean tryLock(String key) {
        return stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 2, TimeUnit.SECONDS);
    }

    /**
     * 释放互斥锁
     *
     * @param key
     * @Title: unLock
     * @Description
     * @Author chs
     * @Return void
     * @Throws
     **/
    public void unLock(String key) {
        stringRedisTemplate.delete(key);

    }
}
